package regsrv;

import java.rmi.AccessException;
import java.rmi.NoSuchObjectException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

import msgcomm.Hooks;
import msgcomm.NameServer;
import msgcomm.Receiver;
import msgcomm.RmiReceiver;
import msgcomm.messages.Message;
import msgcomm.messages.Payload;
import msgcomm.messages.RequestPayload;
import msgcomm.messages.RequestRefreshName;
import msgcomm.transceivers.RmiReliableCausalOrderingMulticast;
import msgcomm.transceivers.RmiUnicast;

import cluedo.messages.NotifyAddPlayGroup;
import cluedo.messages.NotifyChangePlayGroupOwner;
import cluedo.messages.NotifyDelPlayGroup;
import cluedo.messages.RequestAddPlayGroup;
import cluedo.messages.RequestAddPlayer;
import cluedo.messages.RequestChangePlayGroupOwner;
import cluedo.messages.RequestDelPlayGroup;
import cluedo.messages.RequestDelPlayer;
import cluedo.messages.RequestRefreshPlayGroup;



import common.CommonConstants;

/**
 * Il Registration Server implementando {@link RegistrationServerIntf}. Si 
 * rimanda a {@link RegistrationServerIntf} per ulteriori dettagli sul 
 * Registration Server.
 * Si occupa inoltre della creazione di un RMI Registry locale, su cui viene
 * fatto il bind.
 */
public class RegistrationServer extends NameServer implements Hooks {
	
	/*
	 * TODO: invece che usare metodi synchronized, si potrebbe migliorare
	 * l'implementazione utilizzando dei blocchi synchronized sulla singola
	 * mappa utilizzata (players oppure playGroups). E' vero anche che alcuni
	 * metodi utilizzano entrambe le mappe... quindi potrebbe non essere cosi'
	 * importante. 
	 */
	/**
	 * Rmi Registry
	 */
	private Registry registry;
	
	/**
	 * Mappa fra nomi di gruppi di gioco (String) e reicever del player
	 * proprietario (che e' a sua volta una entry che mappa da nome
	 * del transceiver del proprietario e receiver del proprietario)
	 */
	private HashMap<String, Map.Entry<String, Receiver>> playGroups;
	/**
	 * Mappa da nome a receiver registrati
	 */
	private HashMap<String, Receiver> players;
	
	/**
	 * Timer per i gruppi.
	 * Mappa da nome gruppo al corrispondente timer
	 */
	private HashMap<String, LeaseTimer> groupLeases;
	
	/**
	 * Transceiver del Registration Server
	 */
	private RmiUnicast myTrans;
	
	/**
	 * Thread consumatore di messaggi
	 */
	private Consumer consumer;
	
	
	/** 
	 * Avvia RMI Registry sulla porta di default (1099) ed avvia il 
	 * Registration Server.
	 * Non necessita di argomenti.
	 * 
	 * Se avviato da linea di comando:
	 * java -Djava.security.policy=../security.policy cluedo/regsrv/RegistrationServer
	 * 
	 * Se si utilizzano i "Launch configuration" di Eclipse, aggiungere il 
	 * seguente argomento per la VM:
	 * -Djava.security.policy=security.policy
	 */
	public static void main(String[] args){
		
        RegistrationServer reg;
		try {
			reg = new RegistrationServer();
			
			 // premi invio per terminare
			Scanner scan;
			scan= new Scanner(System.in);
			reg.log.print("Premi invio in qualsiasi momento per terminare il Registration Server.");
			reg.log.print();
			scan.nextLine();
			
			reg.log.print("Terminazione del Registration Server");
			reg.close();
			scan.close();
			
		} catch (RemoteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
        
	}

	/**
	 * Crea una nuova istanza del Registration Server.
	 * Non fa export o bind di esso.
	 * @throws RemoteException 
	 */
	public RegistrationServer() throws RemoteException {
		super();
		
		myTrans= new RmiUnicast(CommonConstants.REGSRV, this);
		myTrans.setLogger(log);
		
		RmiReceiver stub= null;
		boolean exported;
		
		exported= false;
		
		// security manager
        if (System.getSecurityManager() == null) {
        	log.print("Installazione Security Manager");
            System.setSecurityManager(new SecurityManager());
        } else {
        	log.print("Security Manager gia' installato");
        }
        
        // avvio di rmiregistry
        try {
            LocateRegistry.createRegistry(1099); 
            log.print("Java RMI registry creato");
        } catch (RemoteException e) {
            //do nothing, error means registry already exists
        	log.print("Java RMI registry gia' esistente");
        }
        
        try {
	        // esportazione
        	log.print("Esportazione");
	 		stub = (RmiReceiver) UnicastRemoteObject.exportObject(myTrans, 0);
	 		exported= true;
			
			// rebind
	 		log.print("Rebind di " + CommonConstants.REGSRV);
	    	registry = LocateRegistry.getRegistry("localhost");
			registry.rebind(CommonConstants.REGSRV, stub);
	
			log.print("Registration Server pronto!");
			
			// timer
			playGroups= new HashMap<String, Map.Entry<String,Receiver>>();
			groupLeases= new HashMap<String, LeaseTimer>();
			
			// mappa dei player
			players= new HashMap<String, Receiver>();
			
			// consumatore
			consumer= new Consumer();
			consumer.start();
			
        } catch (RemoteException e) {
        	e.printStackTrace();
        	// se non e' stato fatto unexport lo faccio per far terminare il processo
        	if (exported==true) {
        		log.print("Terminazione anormale ma oggetto remoto ancora esportato: rimuovo l'esportazione");
        		try {
					UnicastRemoteObject.unexportObject(myTrans, true);
				} catch (NoSuchObjectException e1) {
					e1.printStackTrace();
				}
        	}
        	throw e;
        }
	}
    
	/**
	 * Ferma il Registration Server (unexport e unbind del transceiver)
	 */
	public void close() {

		try {
			// fermo il consumatore
			consumer.interrupt();
			
			// unbind
			log.print("Unbind di " + CommonConstants.REGSRV);
			registry.unbind(CommonConstants.REGSRV);
			
			// unexport
			log.print("Rimozione esportazione (forzata)");
			boolean h= UnicastRemoteObject.unexportObject(myTrans, true);
			if (h) {
				log.print("UnicastRemoteObject(regSrv, true) ha restituito " + h +
						": operazione eseguita con successo");
			} else {
				System.err.println("UnicastRemoteObject(regSrv, true) ha restituito " + h +
						": operazione non eseguita perche' sono presenti chiamate pendenti (nonostante abbia forzato l'operazione...)");
			}
		} catch (AccessException e) {
			e.printStackTrace();
		} catch (RemoteException e) {
			e.printStackTrace();
		} catch (NotBoundException e) {
			e.printStackTrace();
		}
	}
	
	public RequestAddPlayer addRegistration(String name, Receiver transName) {
		RequestAddPlayer resp;
		if (!addName(name)) {
			// player gia' presente
			log.printErr("Registrazione di \"" + name + "\"... player gia' presente: operazione annullata");
			resp= new RequestAddPlayer(name, transName, false, null);
		} else {
			// aggiunto il player
			log.print("Registrazione di \"" + name + "\"...");
			players.put(name, transName);
			log.print("Players registrati: " + players.keySet());
			
			// non devo notificare nessuno del nuovo utente

			// restituisco l'attuale insieme dei proprietari di gruppi
			resp= new RequestAddPlayer(name, transName, true, playGroups);
		}
		return resp;
	}

	public void delRegistration(String name) {
		log.print("Cancellazione registrazione di \"" + name + "\"");
		delName(name);
		players.remove(name);
		log.print("Players registrati: " + players.keySet());
		
		// non devo notificare nessuno di cio'
	}

	public RequestAddPlayGroup addPlayGroup(String groupName, String owner, Receiver ownerTrans) {
		RequestAddPlayGroup resp;
		
		if (playGroups.containsKey(groupName)) {
			// nome di gruppo di gioco gia' presente
			log.printErr("Creazione gruppo di gioco: nome \"" + groupName + "\" gia' usato");
			resp= new RequestAddPlayGroup(groupName, false);
			
		} else {
			// il player e' gia' proprietario di un gruppo di gioco?
			
			boolean found= false;
			for (Map.Entry<String, Receiver> entry : playGroups.values()) {
				if (entry.getKey().equals(owner)) {
					found= true;
					break;
				}
			}
			
			if (found) {
				// il player e' gia' proprietario di un gruppo di gioco
				log.printErr("Creazione gruppo di gioco: il player \"" + owner 
						+ "\" e' gia' proprietario di un altro gruppo");
				resp= new RequestAddPlayGroup(groupName, false);
			} else {
				
				// aggiungo il nuovo gruppo di gioco
				log.print("Creazione gruppo di gioco \"" + groupName + "\"...");
				playGroups.put(groupName, new AbstractMap.SimpleEntry<String, Receiver>(owner, ownerTrans));
				log.print("Gruppi registrati: " + playGroups.keySet());
				
				// aggiungo il timer per il gruppo
				LeaseTimer lt= new LeaseTimer(groupName, new GroupTimer(groupName));
				groupLeases.put(groupName, lt);
	
				// devo notificare tutti i player registrati
				NotifyAddPlayGroup notify= new NotifyAddPlayGroup(groupName, owner, ownerTrans);
				for (Map.Entry<String, Receiver> entry : players.entrySet()) {
					try {
						myTrans.sendAndDeliver(entry.getKey(), entry.getValue(), notify);
					} catch (Exception e) {
						log.printErr("Invio notifica aggiunta gruppo \"" + groupName + "\" al player \"" + entry.getKey() + "\" non riuscita.");
					}
				}
				
				resp= new RequestAddPlayGroup(groupName, true);
			}
		}
		return resp;
	}

	public void delPlayGroup(String groupName) {
		log.print("Cancellazione gruppo di gioco \"" + groupName + "\"");
		playGroups.remove(groupName);
		log.print("Gruppi registrati: " + playGroups.keySet());


		// rimozione timer
		if (groupLeases.containsKey(groupName)) {
			LeaseTimer lt= groupLeases.get(groupName);
			lt.stop();
			groupLeases.remove(groupName);
		}
		
		// devo notificare tutti i player registrati
		NotifyDelPlayGroup notify= new NotifyDelPlayGroup(groupName);
		for (Map.Entry<String, Receiver> entry : players.entrySet()) {
			try {
				myTrans.sendAndDeliver(entry.getKey(), entry.getValue(), notify);
			} catch (Exception e) {
				log.printErr("Invio notifica rimozione gruppo \"" + groupName + "\" al player \"" + entry.getKey() + "\" non riuscita.");
			}
		}	
	}

	public void changePlayGroupOwner(String groupName, String newOwnerName, Receiver newOwnerTrans) {
		
		log.print("Cambiato il proprietario del gruppo \"" + groupName + "\"");
		playGroups.put(groupName, new AbstractMap.SimpleEntry<String, Receiver>(newOwnerName, newOwnerTrans));
		
		// rinfresco automaticamente il timer
		refreshPlayGroup(groupName);
		
		
		// devo notificare tutti i player registrati
		NotifyChangePlayGroupOwner notify= new NotifyChangePlayGroupOwner(groupName, newOwnerName, newOwnerTrans);
		for (Map.Entry<String, Receiver> entry : players.entrySet()) {
			try {
				myTrans.sendAndDeliver(entry.getKey(), entry.getValue(), notify);
			} catch (Exception e) {
				log.printErr("Invio notifica cambio proprietario del gruppo " + groupName + " al player \"" + entry.getKey() + "\" non riuscita.");
			}
		}
	}
	
	public void refreshPlayGroup(String groupName) {
		if (groupLeases.containsKey(groupName)) {
			LeaseTimer lt= groupLeases.get(groupName);
			lt.restart();
		}
	}

	@Override
	public Message respondToRequest(Message msg) {
		Payload payload= msg.getPayload();
		
		RequestPayload resp;
		
		if (payload instanceof RequestAddPlayer) {
			RequestAddPlayer data= (RequestAddPlayer) payload;
			resp= addRegistration(data.name, data.transName);
			
		} else if (payload instanceof RequestDelPlayer) {
			RequestDelPlayer data= (RequestDelPlayer) payload;
			delRegistration(data.name);
			resp= data;
			
		} else if (payload instanceof RequestRefreshName) {
			RequestRefreshName data= (RequestRefreshName) payload;
			refreshName(data.name);
			resp= data;
			
		} else if (payload instanceof RequestAddPlayGroup) {
			RequestAddPlayGroup data= (RequestAddPlayGroup) payload;
			resp= addPlayGroup(data.groupName, data.ownerName, data.ownerTrans);
			
		} else if (payload instanceof RequestDelPlayGroup) {
			RequestDelPlayGroup data= (RequestDelPlayGroup) payload;
			delPlayGroup(data.groupName);
			resp= data;
			
		} else if (payload instanceof RequestRefreshPlayGroup) {
			RequestRefreshPlayGroup data= (RequestRefreshPlayGroup) payload;
			refreshPlayGroup(data.groupName);
			resp= data;
			
		} else if (payload instanceof RequestChangePlayGroupOwner) {
			RequestChangePlayGroupOwner data= (RequestChangePlayGroupOwner) payload;
			changePlayGroupOwner(data.groupName, data.newOwnerName, data.newOwnerTrans);
			resp= data;
			
		} else {
			log.printErr("Messaggio " + msg + " non gestito");
			resp= new RequestPayload() {
				private static final long serialVersionUID = 3598285333085297292L;};
		}

		return new Message(CommonConstants.REGSRV, resp);
	}
	
	private class GroupTimer implements Runnable {
		
		private String groupName;
		
		public GroupTimer(String groupName) {
			this.groupName= groupName;
		}
		
		@Override
		public void run() {
			if (!enable) return;
			log.print("Rilevato crash: timeout " + this);
			
			delPlayGroup(groupName);
		}
	}
	
	/**
	 * Consumatori dei messaggi.
	 * Nota che non viene gestito alcun tipo di messaggio
	 */
	private class Consumer extends Thread {
		public Consumer() {
			super("Messages-Consumer");
		}
		@Override
		public void run() {
			Message msg;
			while (!isInterrupted()) {
				try {
					msg = myTrans.getNextMessage();
					log.printErr(msg.getSender() + ", vedi di piantarla di mandarmi messaggi!!! Tanto non li gestisco :-P");
				} catch (InterruptedException e) {
					// mi hanno interrotto
					log.print("Catturata interruzione");
				}
				
			}
		}
	}
	
	@Override
	public synchronized void delName(String name){
		
		super.delName(name);
		players.remove(name);
		log.print("Rimosso il player "+name);
		
	}

	@Override
	public void hookJoinGroup(Map<String, Receiver> group) {
		
	}

	@Override
	public boolean hookCanItJoin(String name) {
		return false;
	}
}
